package baseFx.database;

import baseFx.common.Assert;
import baseFx.common.EventDispatcher;
import baseFx.database.query.PageData;
import baseFx.database.query.PageSet;

import java.lang.reflect.Array;
import java.util.*;

public abstract class AbstractSql implements Sql {
    private static final String beforeDispatcherKey = UUID.randomUUID().toString();
    private static final String afterDispatcherKey = UUID.randomUUID().toString();
    private static final String errorDispatcherKey = UUID.randomUUID().toString();
    private static final String tranBeforeDispatcherKey = UUID.randomUUID().toString();
    private static final String tranAfterDispatcherKey = UUID.randomUUID().toString();

    private EventDispatcher<SqlBeforeArgs> beforeDispatcher = new EventDispatcher<>(beforeDispatcherKey);
    private EventDispatcher<SqlAfterArgs> afterDispatcher = new EventDispatcher<>(afterDispatcherKey);
    private EventDispatcher<SqlErrorArgs> errorDispatcher = new EventDispatcher<>(errorDispatcherKey);
    private EventDispatcher<TransFinishArgs> tranBeforeDispatcher = new EventDispatcher<>(tranBeforeDispatcherKey);
    private EventDispatcher<TransFinishArgs> tranAfterDispatcher = new EventDispatcher<>(tranAfterDispatcherKey);
    private int transactionCount;
    private Map<String, Object> data = new HashMap<>();

    protected abstract int onExecute(SqlParam param);

    protected abstract <T> List<T> onQuery(Class<T> clazz, SqlParam param);

    protected abstract MultipleReader onMultiple(SqlParam param);

    protected abstract <T> void onPager(Class<T> clazz, PageSet pageSet, SqlParam param);

    protected abstract <T> CursorReader<T> onCursor(Class<T> clazz, SqlParam param, int fetchSize, boolean independent);

    protected abstract void beginTransaction(TransactionLevel level);

    protected abstract void commitTransaction();

    protected abstract void rollbackTransaction();

    protected abstract String placeholder(String name);

    protected AbstractSql() {
        eventBefore().add((s, e) -> {
            replaceCollectionParam(e.param);
        });
    }

    protected void replaceCollectionParam(SqlParam param) {
        if (param.args.isEmpty()) {
            return;
        }
        Map<String, Object> newMap = new LinkedHashMap<>();
        String newSql = param.command;
        for (String name : param.args.keySet()) {
            Object value = param.args.get(name);
            List items = valueToCollection(value);
            if (items == null) {
                newMap.put(name, value);
                continue;
            }
            List<String> ps = new ArrayList<>();
            for (int i = 0; i < items.size(); i++) {
                String newName = name + "_" + (i + 1);
                newMap.put(newName, items.get(i));
                ps.add(paramName(newName));
            }
            String inSql = "(" + String.join(",", ps) + ")";
            newSql = newSql.replace(paramName(name), inSql);
        }
        param.command = newSql;
        param.args.clear();
        param.args.putAll(newMap);
    }

    private List valueToCollection(Object value) {
        List list = new ArrayList();
        if (value != null && value.getClass().isArray()) {
            for (int i = 0; i < java.lang.reflect.Array.getLength(value); i++) {
                list.add(Array.get(value, i));
            }
        } else if (value instanceof Collection) {
            list.addAll((Collection) value);
        } else if (value instanceof Enumeration) {
            Enumeration enumeration = (Enumeration) value;
            while (enumeration.hasMoreElements()) {
                list.add(enumeration.nextElement());
            }
        } else {
            return null;
        }
        return list;
    }

    @Override
    public Map<String, Object> userdata() {
        return data;
    }


    @Override
    public int execute(SqlParam param) {
        SqlBeforeArgs before = new SqlBeforeArgs(SqlOperateType.execute, param);
        try {
            onBefore(before);
            if (before.result == null) {
                before.result = onExecute(before.param);
            }
            SqlAfterArgs after = before.toAfter();
            onAfter(after);
            return (int) after.result;
        } catch (Exception ex) {
            onError(new SqlErrorArgs(before, ex));
            throw ex;
        }
    }

    @Override
    public <T> List<T> query(Class<T> clazz, SqlParam param) {
        SqlBeforeArgs before = new SqlBeforeArgs(SqlOperateType.query, param);
        try {
            onBefore(before);
            if (before.result == null) {
                before.result = onQuery(clazz, before.param);
            }
            SqlAfterArgs after = before.toAfter();
            onAfter(after);
            return (List<T>) after.result;
        } catch (Exception ex) {
            onError(new SqlErrorArgs(before, ex));
            throw ex;
        }
    }

    @Override
    public MultipleReader multiple(SqlParam param) {
        SqlBeforeArgs before = new SqlBeforeArgs(SqlOperateType.multiple, param);
        try {
            onBefore(before);
            if (before.result == null) {
                before.result = onMultiple(before.param);
            }
            SqlAfterArgs after = before.toAfter();
            onAfter(after);
            return (MultipleReader) after.result;
        } catch (Exception ex) {
            onError(new SqlErrorArgs(before, ex));
            throw ex;
        }
    }

    @Override
    public <T> PageData<T> pager(Class<T> clazz, PageSet pageSet, SqlParam param) {
        onPager(clazz, pageSet, param);
        try (MultipleReader reader = multiple(param)) {
            int sum = reader.readOne(int.class);
            List<T> data = reader.read(clazz);
            return new PageData<>(pageSet, data, sum);
        }
    }

    @Override
    public <T> CursorReader<T> cursor(Class<T> clazz, SqlParam param, int fetchSize, boolean independent) {
        SqlBeforeArgs before = new SqlBeforeArgs(SqlOperateType.cursor, param);
        try {
            onBefore(before);
            if (before.result == null) {
                before.result = onCursor(clazz, before.param, fetchSize, independent);
            }
            SqlAfterArgs after = before.toAfter();
            onAfter(after);
            return (CursorReader<T>) after.result;
        } catch (Exception ex) {
            onError(new SqlErrorArgs(before, ex));
            throw ex;
        }
    }

    @Override
    public Transaction transaction(TransactionLevel level) {
        if (transactionCount == 0) {
            beginTransaction(level);
        }
        transactionCount++;
        return new Transaction() {
            private boolean finish;
            private Sql sql = AbstractSql.this;

            @Override
            public void close() {
                if (finish == false) {
                    if (inTransaction()) {
                        rollback(true);
                    }
                }
                sql = null;
            }

            @Override
            public void commit() {
                if (sql.isClosed()) {
                    return;
                }
                Assert.isTrue(transactionCount > 0, "事务已关闭");
                if (transactionCount == 1) {
                    onTransactionBefore(true);
                    commitTransaction();
                    transactionCount = 0;
                    onTransactionAfter(true);
                } else {
                    transactionCount--;
                }
                finish = true;
            }

            @Override
            public void rollback(boolean close) {
                if (sql.isClosed()) {
                    return;
                }
                Assert.isFalse(finish, "事务已关闭");
                onTransactionBefore(false);
                rollbackTransaction();
                transactionCount = 0;
                onTransactionAfter(false);
                finish = true;
                if (close) {
                    sql.close();
                }
            }
        };
    }

    @Override
    public boolean inTransaction() {
        return transactionCount > 0;
    }

    @Override
    public EventDispatcher<SqlAfterArgs> eventAfter() {
        return afterDispatcher;
    }

    @Override
    public EventDispatcher<SqlBeforeArgs> eventBefore() {
        return beforeDispatcher;
    }

    @Override
    public EventDispatcher<SqlErrorArgs> eventError() {
        return errorDispatcher;
    }

    @Override
    public EventDispatcher<TransFinishArgs> eventTransactionAfter() {
        return tranAfterDispatcher;
    }

    @Override
    public EventDispatcher<TransFinishArgs> eventTransactionBefore() {
        return tranBeforeDispatcher;
    }

    @Override
    public void close() {
        if (inTransaction()) {
            rollbackTransaction();
        }
        if (beforeDispatcher != null) {
            beforeDispatcher.clear();
            beforeDispatcher = null;
        }
        if (afterDispatcher != null) {
            afterDispatcher.clear();
            afterDispatcher = null;
        }
        if (errorDispatcher != null) {
            errorDispatcher.clear();
            errorDispatcher = null;
        }
        if (tranBeforeDispatcher != null) {
            tranBeforeDispatcher.clear();
            tranBeforeDispatcher = null;
        }
        if (tranAfterDispatcher != null) {
            tranAfterDispatcher.clear();
            tranAfterDispatcher = null;
        }
        if (data != null) {
            data.clear();
            data = null;
        }
    }

    protected void onBefore(SqlBeforeArgs args) {
        beforeDispatcher.fire(beforeDispatcherKey, this, args);
    }

    protected void onAfter(SqlAfterArgs args) {
        afterDispatcher.fire(afterDispatcherKey, this, args);
    }

    protected void onError(SqlErrorArgs args) {
        errorDispatcher.fire(errorDispatcherKey, this, args);
    }

    protected void onTransactionBefore(boolean isCommit) {
        TransFinishArgs args = new TransFinishArgs(isCommit);
        tranBeforeDispatcher.fire(tranBeforeDispatcherKey, this, args);
    }

    protected void onTransactionAfter(boolean isCommit) {
        TransFinishArgs args = new TransFinishArgs(isCommit);
        tranAfterDispatcher.fire(tranAfterDispatcherKey, this, args);
    }
}
